package com.google.precinct.server;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.servlet.http.Cookie;

import org.apache.commons.lang.WordUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.precinct.common.AddressInfo;
import com.google.precinct.common.CityItem;
import com.google.precinct.common.PrecinctInfo;
import com.google.precinct.common.PrecinctService;

/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class PrecinctServiceImpl extends RemoteServiceServlet implements
		PrecinctService {

//	@Override
//	public void addDraftPrecinctLocation(
//			String precinctId,
//			double lat,
//			double lng) {
//		if (precinctId == null || precinctId.isEmpty()) {
//			throw new RuntimeException("Invalid Input!");
//		}
//		DraftPrecinct precinct = new DraftPrecinct(precinctId,
//				getThreadLocalRequest().getRemoteHost(),
//				lat,
//				lng);
//		EMF.get().persist(precinct);
//	}

	@Override
	public List<PrecinctInfo> getPrecinctsInBounds(double swLat, double swLng,
			double neLat, double neLng) {

		return new ArrayList<PrecinctInfo>();
	}

	@Override
	public SuggestOracle.Response getCitySuggestions(SuggestOracle.Request req) {


		// Extract from DB/Memcache
		String q = req.getQuery().toUpperCase();
		Key cityCmplteKey = KeyUtils.cityCompletion(q.toUpperCase(), (q.length() > 2 ? 2 : q.length()));

		// Get from Memcache if present
		MemcacheService mem = MemcacheServiceFactory.getMemcacheService();
		Object valueObj = mem.get(KeyUtils.memcache(cityCmplteKey));
		JSONArray value = null;
		try {
			if (valueObj != null) {
				value = new JSONArray(valueObj.toString());
			}
			if (value == null) {
				// Get from the Data Store
				DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
				try {
					Entity cityEnt = ds.get(cityCmplteKey);
					value = new JSONObject(new String(((Blob) cityEnt.getProperty("completion"))
							.getBytes()))
							.getJSONArray("list");
					if (value != null) {
						// Store to MemCache
						mem.put(KeyUtils.memcache(cityCmplteKey), value.toString(), Expiration.byDeltaSeconds(60 * 15));
					}
				} catch (EntityNotFoundException e) {}
			}
		} catch (JSONException e1) {
			e1.printStackTrace();
		}

		SuggestOracle.Response resp = new SuggestOracle.Response();
		if (value == null) {
			resp.setSuggestions(new ArrayList<CityItem>());
		} else {
			List<CityItem> suggestions = new ArrayList<CityItem>();
			// Translate Blob -> JSON Array
			try {
				int i = 0;
				// Filter out only the query matching strings
				while (suggestions.size() < req.getLimit()) {
					if (i >= value.length()) {
						break;
					}
					JSONObject row = value.getJSONObject(i);i++;

					String province = row.getString("P");
					String city = row.getString("C");
					String suggestText = WordUtils.capitalizeFully(city) + ", " +
					WordUtils.capitalizeFully(province);

					if (!suggestText.toUpperCase().startsWith(q)) {
						continue;
					}
					CityItem item = new CityItem(row.getString("R"), suggestText, row.getLong("voters"));
					item.setProvinceKey(province);
					item.setCityKey(city);
					suggestions.add(item);
				}
			} catch (JSONException e) {
				e.printStackTrace();
			}
			Collections.sort(suggestions, new Comparator<CityItem>() {
				@Override
				public int compare(CityItem o1, CityItem o2) {
					return o1.getCityProvinceName().compareTo(o2.getCityProvinceName());
				}
			});
			resp.setSuggestions(suggestions);
		}
		return resp;
	}

	@Override
	public List<AddressInfo> getAddressesOnCity(String region, String province,
			String city, boolean unmappedOnly, boolean mappedLakingVotesOnly, int limit) {
    // Get all of the Addresses for a particular city
		Query qCityAllAddress = new Query(KeyUtils.kndAddress,
				KeyUtils.precinctInfoCity(
						KeyUtils.precinctInfoProvince(
								KeyUtils.precinctInfo(region), province), city));
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		List<Entity> list = ds.prepare(qCityAllAddress)
				.asList(FetchOptions.Builder.withLimit(300));

		List<AddressInfo> results = new ArrayList<AddressInfo>();
		for (Entity address : list) {
			if (results.size() >= limit) break;
			AddressInfo item = getAddressInfoFromEntity(address);
			if (unmappedOnly && address.hasProperty("lat")) {
				if (mappedLakingVotesOnly &&
						(item.getConVotes() >= 2 || item.getProVotes() >= 2)) {
					continue;
				} else {
					continue;
				}
			}
			results.add(item);
		}
		return results;
	}

	private AddressInfo getAddressInfoFromEntity(Entity address) {
		String region = address.getKey().getParent().getParent().getParent().getName();
		String province = address.getKey().getParent().getParent().getName();
		String city = address.getKey().getParent().getName();

		int proVotes = (address.hasProperty("proVotes") ? ((Long) address.getProperty("proVotes")).intValue() : 0);
		int conVotes = (address.hasProperty("conVotes") ? ((Long) address.getProperty("conVotes")).intValue() : 0);
		AddressInfo item = new AddressInfo(region, province, city, address.getKey().getName());
		item.setClusterCount(((Long) address.getProperty("clusterCount")).intValue());
		item.setConVotes(conVotes);
		item.setLat(address.hasProperty("lat") ? (Double) address.getProperty("lat") : 0.0);
		item.setLng(address.hasProperty("lng") ? (Double) address.getProperty("lng") : 0.0);
		item.setMapped(address.hasProperty("lat") && proVotes >= 2);
		item.setProVotes(proVotes);
		item.setVotersCount((Long) address.getProperty("votersCount"));
		item.setDisplayAddress(enhanceAddress(province, city, item.getAddress()));
		return item;
	}

	private String enhanceAddress(String province, String city, String address) {
		if (address.endsWith(province)) {
			address = smartStrip(address.substring(0, address.length() - province.length()));
		}
		if (address.endsWith(city)) {
			address = smartStrip(address.substring(0, address.length() - city.length()));
		}
		return WordUtils.capitalizeFully(address);
	}

	private String smartStrip(String inputStr) {
		inputStr = inputStr.trim();
		if (inputStr.endsWith(",")) {
			return inputStr.substring(0, inputStr.length() - 1).trim();
		}
		return inputStr;
	}

	@Override
	public void addAddressGeoLocation(AddressInfo addressInfo, double latitude,
			double longitude) {
		// Save who created the entry
		String ip = getThreadLocalRequest().getRemoteAddr();
		String userMarker = getUserIdCookie(getThreadLocalRequest().getCookies());

		if (ip == null || ip.isEmpty() || userMarker == null || userMarker.isEmpty()) {
			throw new RuntimeException("No User identifiable marker. (" + ip + "|" + userMarker + ")");
		}

		Key addressKey = KeyUtils.addressInfo(
				KeyUtils.precinctInfoCity(
					KeyUtils.precinctInfoProvince(
							KeyUtils.precinctInfo(addressInfo.getRegion()),
									addressInfo.getProvince()), addressInfo.getCity()),
											addressInfo.getAddress());
		// Get from the Data Store
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		try {
			Entity addressEnt = ds.get(addressKey);
			// TODO: Merge if geo already exists
			addressEnt.setUnindexedProperty("lat", latitude);
			addressEnt.setUnindexedProperty("lng", longitude);
			addressEnt.setProperty("geopt", new GeoPt((float) latitude, (float) longitude));
			addressEnt.setUnindexedProperty("proVotes", 0);
			addressEnt.setUnindexedProperty("conVotes", 0);

			Key userKey = KeyUtils.userMarker(KeyUtils.userIP(ip), userMarker);
			Entity userLog = new Entity(userKey);
			userLog.setUnindexedProperty("time", System.currentTimeMillis());
			userLog.setUnindexedProperty("action", "plot");
			userLog.setUnindexedProperty("address", addressKey);

			ds.put(Arrays.asList(new Entity[] { addressEnt, userLog }));
		} catch (EntityNotFoundException e) {
			throw new RuntimeException("The address entry was not found. \n" +
					"(" + addressInfo.getRegion() + "| " + addressInfo.getProvince() +
					"| " + addressInfo.getCity() + "| " + addressInfo.getAddress());
		}
	}

	private String getUserIdCookie(Cookie[] cookies) {
		for (Cookie cookie : cookies) {
			if (cookie.getName().equals("__utmc")) {
				return cookie.getValue();
			}
		}
		return null;
	}



	@Override
	public PrecinctInfo getAllPrecinctInfo(CityItem selection,
			String precinctNo) {
		Key precinctKey = KeyUtils.precinctInfoPrecinct(KeyUtils.precinctInfoCity(
						KeyUtils.precinctInfoProvince(
								KeyUtils.precinctInfo(selection.getRegion()),
										selection.getProvinceKey()),
										selection.getCityKey()),
										precinctNo);
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		// Get Precinct Information
		try {
			Entity precinct = ds.get(precinctKey);
			Key clusterKey = (Key) precinct.getProperty("clusterKey");
			if (clusterKey == null) {
				return null;
			}

			// Get Cluster Information
			Entity cluster = ds.get(clusterKey);

			// Get the Address Information
			Key addressKey = KeyUtils.addressInfo(precinctKey.getParent(),
					(String) cluster.getProperty("address"));
			Entity address = ds.get(addressKey);

			// Compose the return Value
			PrecinctInfo precinctInfo = new PrecinctInfo(selection.getRegion(),
					selection.getProvinceKey(),
					selection.getCityKey(),
					precinctNo);
			precinctInfo.setClusterAddress((String) cluster.getProperty("address"));
			precinctInfo.setClusterNumber(((Long) cluster.getProperty("num")).intValue());
			precinctInfo.setClusterVoters((Long) cluster.getProperty("voters"));
			precinctInfo.setVoterCount((Long) precinct.getProperty("voters"));
			precinctInfo.setAddress(getAddressInfoFromEntity(address));

			return precinctInfo;

		} catch (EntityNotFoundException e) {
			return null;
		}
	}
}
